//
//  SwiftNotes.swift
//  Documentation
//
//  Created by Toj on 3/30/23.
//

https://www.jianshu.com/p/a709ba98aad9


// MARK: - get/set方法

// MARK: - willSet/ didSet: 初始化过程不会被调用
swift初始化(init) 设置属性不会调用属性的willSet/ didSet

// MARK: - willSet/ didSet: 初始化强制调用
1. 使用 defer延迟调用
2. 通过函数调用
3. 通过KVC方法赋值后,
   再添加setValueforUndefinedKey方法做特殊处理的方案, 不建议使用太麻烦
   真不行初始化后调用也可以, 为什么那么麻烦呢?


属性观察者，类似于触发器。用来监视属性的除初始化之外的属性值变化，当属性值发生改变时可以对此作出响应。有如下特点：
1，不仅可以在属性值改变后触发didSet，也可以在属性值改变前触发willSet。
2，给属性添加观察者必须要声明清楚属性类型，否则编译器报错。
3，willSet带一个newValue的参数，表示新的属性。
4，didSet带一个oldValue的参数，表示旧的属性。
5，属性初始化时，willSet和didSet不会调用。只有在初始化上下文之外，当设置属性值时才会调用。
6，即使是设置的值和原来值相同，willSet和didSet也会被调用


Swift 一直以来有个非常方便的特性：属性观察者（Property Observer），即属性上的willSet 和 didSet 函数。在 Swift 5.3 中，对 didSet 有一处小的性能优化，在了解这个之前，我们来仔细复习一下 didSet，有一些细节你不一定知道或者记得。
存储属性的观察者
最常见的使用场景是给正在定义的类型中的存储属性添加观察者，如下例所示，willSet 在属性值被更新之前被调用，didSet在属性值更新后被调用。
swift复制代码class Container {
  var items = [Int](repeating: 1, count: 100) {
    willSet {
      print("willSet is called")
      print("current item size: \(items.count)")
      print("new item size: \(newValue.count)")
    }
    didSet {
      print("didSet is called")
      print("current item size: \(items.count)")
      print("old item size: \(oldValue.count)")
    }
  }
}

let container = Container()
container.items = [] // willSet 和 didSet 被调用
在willSet 和 didSet 中可以访问属性本身，并且如果不指定名称的话，willSet 可以通过 newValue访问即将被设置的值，didSet可以通过oldValue访问此次设置之前的属性值。
这里需要注意的是，哪怕设置的值与原来的值相同，willSet 和 didSet都是会被调用的。
有一种情况下属性观察者不会被调用：当前类型的init函数中。假如有以下init函数，在函数中对 items 进行赋值并不会触发willSet 和 didSet。
javascript复制代码class Container {
  init() {
    items = []
  }
}
为什么要制定这一条规则呢？原因在于构造函数本来就是特殊的，假如在构造的时刻触发属性观察者，而属性观察者中又访问了还没被初始化的其它的属性的话，就导致了访问了未完全初始化的对象，Swift 主打安全的初始化就会被破坏。
继承下的属性观察者
在继承情况下的规则有一些不同，但是也好理解。
第一点：在构造函数中，对继承而来的属性设置值会触发父类中的属性观察者的调用。
kotlin复制代码class MyContainer: Container {
  var tag: String
  
  override init() {
    tag = "Leon"
    super.init()
    items = [1,2,3] // 触发父类中的 willSet 和 didSet
  }
}
这时候你可能会有个问号：这里的触发难道不会造成访问一个未完全初始化的对象吗？比如说父类的 didSet 中调用的一个方法被子类 override，而子类的这个方法访问了还没被初始化的子类中声明的属性？
访问未被初始化的子类属性的情况不存在，因为在调用 super.init() 前，子类必须完成自身属性的初始化。实际上，上述代码段中的三行初始化的语句是没有办法调换顺序的。Swift 通过强制初始化顺序来确保在复杂情况下构造函数还是安全的，避免了一些老牌的面向对象语言中存在的问题。
第二点：可以给继承的属性添加属性观察者，哪怕继承的是计算属性：
kotlin复制代码class MyContainer: Container {
  override var items: [Int] {
    didSet {
      print("didSet is called in the subclass")
    }
  }
}
无论父类中的属性是否有属性观察者，作为子类我只添加不替换。属性观察者会按照先父类（若有）再子类的顺序来执行。另外，对于继承的计算属性我们也可以添加属性观察者，因为对于子类来说，给继承的属性添加属性观察者，无需区分它到底是存储属性还是计算属性，对子类来说它就是属性：一对 getter 和 setter 函数。
话说回来，为什么不能为自己声明的计算属性添加 willSet和didSet 呢？那是因为你是个成熟的计算属性了，set本来就是你自身定义的了，你在set 的函数开头写上 willSet 的逻辑、在结束写上 didSet 就起到属性观察者的同样作用了。
属性类型：值与引用
如果属性是一个值类型，调用它的 mutating方法或者直接修改它的值的话会从内到外逐层调用属性观察者。
下面这个例子中，会先调用 items 的 didSet，再调用 container的didSet，注意这个例子中的 Container已经改成值类型了。
scss复制代码struct Container {
  var items = [Int](repeating: 1, count: 100) {
    didSet {
      print("items didSet is called")
    }
  }
}

class ViewController: UIViewController {
  var container = Container() {
    didSet {
      print("container didSet is called")
    }
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    container.items.append(1)
  }
}
假如 Container 是个引用类型，那么只有 items 的 didSet会被调用。引用类型作为属性，只有在该引用被替换的时候才会触发属性观察者。
inout 例行公事
当参数是由 inout 修饰的时候，我们需要知道在函数退出之前，无论有没有修改，属性都会被写回，属性观察者会被调用，这是由 Swift 内存模型所规定的。
swift复制代码struct Container {
  var items = [Int](repeating: 1, count: 100) {
    willSet {
      print("item willSet is called")
    }
    
    didSet {
      print("item didSet is called")
    }
  }
}

func modify(items: inout [Int]) {
   print("actually do nothing")
}

var container = Container()
modify(items:&container.items)
这个例子中，首先会打印的是 actually do nothing，然后是 willSet 和 didSet。
Swift 5.3 didSet的性能优化
在 Swift 5.3 中，提供了一个简单版本的 didSet：如果 oldValue 没有被用到（如上例），Swift 5.3 会直接跳过 oldValue 的创建，这意味着节省了 内存 和 CPU 的开销。
这个改动有很小的可能性会影响到代码兼容性，比如说代码的正确性依赖于属性 的 getter 被调用。要恢复这个行为可以显示地声明变量名字：
javascript复制代码didSet(oldValue) {}
或者这样引用一下：
ini复制代码didSet { _ = oldValue }
我们可以使用计算属性来证明一下这件事情：
kotlin复制代码class Container {
  var items :[Int] {
    get {
      print("getter is called")
      return []
    }
    set {}
  }
}

class MyContainer: Container {
  override var items: [Int] {
    didSet {
      print("didSet is called")
    }
  }
}

let container = MyContainer()
container.items = []
在 Swift 5.3 中，getter 不会被调用，oldValue 不需要生成，性能得到了提升。
应用和提示
属性观察者是个非常实用的工具，除了可以日常进行 debug 或者打日志，还可以用来实现一些简单逻辑，比如说 ViewController 中有一个 person 属性，当它被更新的时候，我可以在 didSet 调用更新界面的逻辑。
另外，我们可以在自己声明的属性的didSet中安全地重新给这个属性设置一个新的值，这不会触发didSet的无限循环调用。
你一定觉得这很棒啊，有什么需要注意的吗？于是开始浪，在继承属性的didSet中也对本属性赋值了新的值，那恭喜你无限循环崩溃了。
你想了下，这怎么可能是我，那还有一种情况可能适合你，在 A 属性的 didSet 中更新 B 属性的值，并在 B 属性的 didSet 中更新 A 属性的值。你是风儿我是沙，缠缠绵绵栈爆炸。
也许这种错误还不是你这种级别的高手会犯的，但一旦代码分支复杂了，联动效果多了，不是谁能一眼看出来了，说不定哪天就变成了蝴蝶效应。无节制的使用 didSet 的联动是明显的代码坏味道，尽管可以通过判等跳过来打破这个循环，临时解决这个问题，但是不让代码往这个方向腐化是每个代码维护者需要关心的事情。
